package com.yonyou.crm.sys.shiro.filters;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.json.JSONObject;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;

import com.alibaba.dubbo.rpc.RpcContext;
import com.yonyou.crm.common.exception.CrmBusinessException;
import com.yonyou.crm.common.login.context.LoginContextConstant;
import com.yonyou.crm.common.rest.constant.HttpCodeConstant;
import com.yonyou.iuap.auth.shiro.StatelessToken;
import com.yonyou.iuap.auth.token.ITokenProcessor;
import com.yonyou.iuap.auth.token.TokenFactory;
import com.yonyou.iuap.auth.token.TokenParameter;
import com.yonyou.iuap.context.InvocationInfoProxy;
import com.yonyou.iuap.log.utils.ThreadCallerIdGenerator;
import com.yonyou.iuap.utils.CookieUtil;
import com.yonyou.iuap.utils.PropertyUtil;

public class StatelessAuthcFilter extends AccessControlFilter {
	private static final Logger log = LoggerFactory.getLogger(StatelessAuthcFilter.class);
	public static final int HTTP_STATUS_AUTH = 306;
	private String sysid;

	@Autowired
	private TokenFactory tokenFactory;
	private String[] esc = { "/logout", "/login", "/formLogin", ".jpg", ".png", ".gif", ".css", ".js", ".jpeg",
			"/oauth_login", "/oauth_approval" };

	private List<String> excludCongtextKeys = Arrays
			.asList(new String[] { "u_sysid", "tenantid", "u_callid", "u_usercode", "token", "u_logints", "u_locale",
					"u_theme", "u_timezone", "current_user_name", "call_thread_id", "current_tenant_id" });

	private List<String> rpcCongtextKeys = Arrays
			.asList(new String[] { 
					LoginContextConstant.UserId, 
					LoginContextConstant.TenantId, 
					LoginContextConstant.RoleId, 
					LoginContextConstant.OrgId, 
					LoginContextConstant.DeptId});

	public void setSysid(String sysid) {
		this.sysid = sysid;
	}

	public void setTokenFactory(TokenFactory tokenFactory) {
		this.tokenFactory = tokenFactory;
	}

	public void setEsc(String[] esc) {
		this.esc = esc;
	}

	public void setExcludCongtextKeys(List<String> excludCongtextKeys) {
		this.excludCongtextKeys = excludCongtextKeys;
	}

	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
			throws Exception {
		return false;
	}

	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
		boolean isAjax = isAjax(request);
		HttpServletRequest hReq = (HttpServletRequest) request;
		if (StringUtils.isEmpty(this.sysid)) {
			throw new Exception(
					"sysid is empty! add sysid parameter to 'StatelessAuthcFilter' bean in application-shiro.xml");
		}
		boolean needCheck = !include(hReq); // css,js,login,logout,pic等不需要校验
		// 直接跳过
		
		// 如果需要校验
		if (needCheck) {
			// 1、客户端发送来的摘要

			HttpServletRequest httpRequest = hReq;
			Cookie[] cookies = httpRequest.getCookies();
			String authority = httpRequest.getHeader("authority");

			// 如果header中包含，则以header为主，否则，以cookie为主
			if (StringUtils.isNotBlank(authority)) {
				Set<Cookie> cookieSet = new HashSet<Cookie>();
				JSONObject headerParam = JSONObject.fromObject(authority);
				Object object = headerParam.get("authority");
				JSONObject authorityJson = JSONObject.fromObject(object);
				Set<String> keySet = authorityJson.keySet();
				for (String key : keySet) {
					Object value = authorityJson.get(key);
					Cookie cookie = new Cookie(key, value+"");
					cookieSet.add(cookie);
				}


				cookies = (Cookie[]) cookieSet.toArray(new Cookie[0]);
			}

			String tokenStr = CookieUtil.findCookieValue(cookies, "token");
			String cookieUserName = CookieUtil.findCookieValue(cookies, "u_usercode");

			String theme = CookieUtil.findCookieValue(cookies, "u_theme");
			String locale = CookieUtil.findCookieValue(cookies, "u_locale");
			String timeZone = CookieUtil.findCookieValue(cookies, "u_timezone");
			String logints = CookieUtil.findCookieValue(cookies, "u_logints");

			String callerThreadId = CookieUtil.findCookieValue(cookies, "u_callid");

			// 2、客户端传入的用户身份
			String username = request.getParameter("userName");
			if ((username == null) && (StringUtils.isNotBlank(cookieUserName))) {
				username = cookieUserName;
			}

			if ((tokenStr == null) || (username == null)) {
				if (isAjax)
					throw new CrmBusinessException("验证失败，请重新登陆");
				else {
					onLoginFail(request, response);
				}
				return false;
			}

			// 3、客户端请求的参数列表
			Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap());

			ITokenProcessor tokenProcessor = this.tokenFactory.getTokenProcessor(tokenStr);
			TokenParameter tp = tokenProcessor.getTokenParameterFromCookie(cookies);
			// 4、生成无状态Token
			StatelessToken token = new StatelessToken(username, tokenProcessor, tp, params, new String(tokenStr));
			try {
				// 登陆验证，主要验证 请求携带的cookie中token 是否与
				// 后台根据tokenProcessor生成的token是否一致：
				// 如果一致验证成功，如果不一致则验证失败
				Subject subject = getSubject(request, response);
				subject.login(token);

				// 设置上下文变量
				InvocationInfoProxy.setSysid(this.sysid);
				InvocationInfoProxy.setTheme(theme);
				InvocationInfoProxy.setLocale(locale);
				if (!StringUtils.isEmpty(timeZone)) {
					InvocationInfoProxy.setTimeZone(timeZone);
				}
				InvocationInfoProxy.setUserid(username);
				InvocationInfoProxy.setLogints(logints);
				InvocationInfoProxy.setTenantid((String) tp.getExt().get("tenantid"));
				InvocationInfoProxy.setToken(tokenStr);
				InvocationInfoProxy.setCallid(callerThreadId);
				
				// 设置上下文携带的额外属性
				initExtendParams(cookies);
				initMDC();
				afterValidate(hReq);
			} catch (Exception e) {
				
				log.error(e.getMessage(), e);
				
				HttpServletResponse hRes = ((HttpServletResponse)response);
				
				if (e instanceof AuthenticationException) {

					if (StringUtils.isNotBlank(tokenStr) && StringUtils.isNotBlank(username)) {
						
						//清空登录信息
						getSubject(request, response).logout();
						
					    tokenFactory.getTokenProcessor(tokenStr).getLogoutCookie(tokenStr, username);
					    
					    for (Cookie cookie : hReq.getCookies()) {
					    	cookie.setMaxAge(0);
					    	cookie.setPath(hReq.getContextPath());
					    	((HttpServletResponse)response).addCookie(cookie);
					    }
					}

				}

				if (isAjax) {
					//端上完成重定向到登录页
					hRes.setStatus(HttpCodeConstant.Unauthorized);
				}
				else {
					onLoginFail(request, response);// 6、登录失败，跳转到登录页
				}
				return false;
			}

			return true;
		}
		return true;
	}

	private boolean isAjax(ServletRequest request) {
		boolean isAjax = false;
		if ((request instanceof HttpServletRequest)) {
			HttpServletRequest rq = (HttpServletRequest) request;
			String requestType = rq.getHeader("X-Requested-With");
			if ((requestType != null) && ("XMLHttpRequest".equals(requestType))) {
				isAjax = true;
			}
		}
		return isAjax;
	}

	protected void onLoginFail(ServletRequest request, ServletResponse response) throws IOException {
		
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		
		httpResponse.setStatus(HttpCodeConstant.Unauthorized);

		redirectToLogin(request, httpResponse);
	}

	protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
		HttpServletRequest hReq = (HttpServletRequest) request;
		String qryString = hReq.getQueryString();

		if ((qryString != null) && (!qryString.isEmpty())) {
			qryString = qryString + "?" + hReq.getQueryString();
		}

		//String rURL = hReq.getRequestURI();
		//rURL = Base64.encodeBase64URLSafeString(rURL.getBytes()); r= rURL 暂时不支持

		// 加入登录前地址
		String loginUrl = getLoginUrl() /*+ "?r=" + rURL*/;
		WebUtils.issueRedirect(request, response, loginUrl);
	}

	public boolean include(HttpServletRequest request) {
		String u = request.getRequestURI();
		if(u.contains("debug")) return true; //测试专用 url中包含dubug字样的 跳过shiro
		for (String e : this.esc) {
			if (u.endsWith(e)) {
				return true;
			}
		}

		String exeludeStr = PropertyUtil.getPropertyByKey("filter_excludes");
		if (StringUtils.isNotBlank(exeludeStr)) {
			String[] customExcludes = exeludeStr.split(",");
			for (String e : customExcludes) {
				if (u.endsWith(e)) {
					return true;
				}
			}
		}

		return false;
	}

	public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception)
			throws Exception {
		super.afterCompletion(request, response, exception);
		InvocationInfoProxy.reset();
		clearMDC();
	}

	/**
	 * 设置上下文中的扩展参数，rest传递上下文时生效，Authorityheader
	 * 中排除固定key的其它信息都设置到InvocationInfoProxy的parameters
	 * 
	 * @param cookies
	 */
	private void initExtendParams(Cookie[] cookies) {
		for (Cookie cookie : cookies) {
			String cname = cookie.getName();
			String cvalue = cookie.getValue();
			if (!this.excludCongtextKeys.contains(cname)) {
				InvocationInfoProxy.setParameter(cname, cvalue);
			}
			
			if (rpcCongtextKeys.contains(cname)) {
				RpcContext.getContext().setAttachment(cname, cvalue);
			}
		}
	}

	private void initMDC() {
		String username = "";
		Subject subject = SecurityUtils.getSubject();
		if ((subject != null) && (subject.getPrincipal() != null)) {
			username = (String) SecurityUtils.getSubject().getPrincipal();
		}
		// MDC中记录用户信息
		MDC.put("current_user_name", username);
		String call_thread_id = InvocationInfoProxy.getCallid();
		if (StringUtils.isBlank(call_thread_id)) {
			call_thread_id = ThreadCallerIdGenerator.genCallerThreadId();
			InvocationInfoProxy.setCallid(call_thread_id);
		} else {
			MDC.put("call_thread_id", call_thread_id);
		}

		MDC.put("current_tenant_id", InvocationInfoProxy.getTenantid());

		initCustomMDC();

	}

	protected void initCustomMDC() {
	}

	protected void afterValidate(HttpServletRequest hReq) {
	}

	protected void clearMDC() {
		MDC.remove("current_user_name");
		MDC.remove("call_thread_id");
		MDC.remove("current_tenant_id");

		clearCustomMDC();
	}

	protected void clearCustomMDC() {
	}
}